热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

全名|推断_类加载器释疑

篇首语:本文由编程笔记#小编为大家整理,主要介绍了类加载器释疑相关的知识,希望对你有一定的参考价值。1 由不同的类加载器加载的指定类

篇首语:本文由编程笔记#小编为大家整理,主要介绍了类加载器释疑相关的知识,希望对你有一定的参考价值。




1   由不同的类加载器加载的指定类还是相同的类型吗?


      在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。我们可以用两个自定义类加载器去加载某自定义类型(注意不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。这个大家可以写两个自定义的类加载器去加载相同的自定义类型,然后做个判断;同时,可以测试加载java.*类型,然后再对比测试一下测试结果。



2   在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?

Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对应的jdk的代码:



//java.lang.Class.java
publicstatic Class forName(String className) throws ClassNotFoundException
return forName0(className, true, ClassLoader.getCallerClassLoader());


//java.lang.ClassLoader.java
// Returns the invoker's class loader, or null if none.
static ClassLoader getCallerClassLoader()
// 获取调用类(caller)的类型
Class caller = Reflection.getCallerClass(3);
// This can be null if the VM is requesting it
if (caller == null)
return null;

// 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader
return caller.getClassLoader0();


//java.lang.Class.java
//虚拟机本地实现,获取当前类的类加载器,前面介绍的Class的getClassLoader()也使用此方法
native ClassLoader getClassLoader0();





3    在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是谁?


     前面讲过,在不指定父类加载器的情况下,默认采用系统类加载器。可能有人觉得不明白,现在我们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:



//摘自java.lang.ClassLoader.java
protected ClassLoader()
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkCreateClassLoader();

this.parent = getSystemClassLoader();
initialized = true;





我们再来看一下对应的getSystemClassLoader()方法的实现:


private static synchronized void initSystemClassLoader()
//...
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
scl = l.getClassLoader();
//...





我们可以写简单的测试代码来测试一下:


System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());




本机对应输出如下:


sun.misc.Launcher$AppClassLoader@73d16e93




所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论:即使用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:
  1. /lib下的类;
  2. /lib/ext下或者由系统变量java.ext.dir指定位置中的类;
  3. 当前工程类路径下或者由系统变量java.class.path指定位置中的类。 



4 在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?


JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:
  即使用户自定义类加载器不指定父类加载器,那么,同样可以加载到/lib下的类,但此时就不能够加载/lib/ext目录下的类了。
  说明:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题5。






5 编写自定义类加载器时,一般有哪些注意点?


1、一般尽量不要覆写已有的loadClass(...)方法中的委派逻辑
  一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。举一个例子来验证该问题:






//用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)
public class WrongClassLoader extends ClassLoader

public Class loadClass(String name) throws ClassNotFoundException
return this.findClass(name);


protected Class findClass(String name) throws ClassNotFoundException
// 假设此处只是到工程以外的特定目录D:\\library下去加载类
// 具体实现代码省略



     通过前面的分析我们已经知道,这个自定义类加载器WrongClassLoader的默认类加载器是系统类加载器,但是现在问题4种的结论就不成立了。大家可以简单测试一下,现在/lib、/lib/ext和工程类路径上的类都加载不上了。







//问题5测试代码一
public class WrongClassLoaderTest

publicstaticvoid main(String[] args)
try
WrongClassLoader loader = new WrongClassLoader();
Class classLoaded = loader.loadClass("beans.Account");
System.out.println(classLoaded.getName());
System.out.println(classLoaded.getClassLoader());
catch (Exception e)
e.printStackTrace();








这里D:"classes"beans"Account.class是物理存在的。输出结果:






java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.(FileInputStream.java:106)
at WrongClassLoader.findClass(WrongClassLoader.java:40)
at WrongClassLoader.loadClass(WrongClassLoader.java:29)
at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
at java.lang.ClassLoader.defineClass(ClassLoader.java:400)
at WrongClassLoader.findClass(WrongClassLoader.java:43)
at WrongClassLoader.loadClass(WrongClassLoader.java:29)
at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
at java.lang.ClassLoader.defineClass(ClassLoader.java:400)
at WrongClassLoader.findClass(WrongClassLoader.java:43)
at WrongClassLoader.loadClass(WrongClassLoader.java:29)
at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)


这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于覆写loadClass()引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。




//问题5测试二
//用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)
public class WrongClassLoader extends ClassLoader

protected Class findClass(String name) throws ClassNotFoundException
//假设此处只是到工程以外的特定目录D:\\library下去加载类
//具体实现代码省略



将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输出结果如下:







beans.Account
WrongClassLoader@1c78e57



       2、正确设置父类加载器

  通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。

  
3、保证findClass(String name)方法的逻辑正确性

  事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。







6 如何在运行时判断系统类加载器能加载哪些路径下的类?


  一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到。
  二是可以直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 :System.getProperty("java.class.path")。






7 如何在运行时判断标准扩展类加载器能加载哪些路径下的类?


  方法之一:


import java.net.URL;
import java.net.URLClassLoader;

public class ClassLoaderTest

/**
* @param args the command line arguments
*/
public static void main(String[] args)
try
URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (int i = 0; i System.out.println(extURLs[i]);

catch (Exception e)
//…








本机对应输出如下:






file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/zipfs.jar







开发自己的类加载器

  在前面介绍类加载器的代理委派模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。
  方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。
  类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。


  在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。









1 文件系统类加载器


  第一个类加载器用来加载存储在文件系统上的Java字节代码。完整的实现如下所示。


package classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

// 文件系统类加载器
public class FileSystemClassLoader extends ClassLoader

private String rootDir;

public FileSystemClassLoader(String rootDir)
this.rootDir = rootDir;


// 获取类的字节码
@Override
protected Class findClass(String name) throws ClassNotFoundException
byte[] classData = getClassData(name); // 获取类的字节数组
if (classData == null)
throw new ClassNotFoundException();
else
return defineClass(name, classData, 0, classData.length);



private byte[] getClassData(String className)
// 读取类文件的字节
String path = classNameToPath(className);
try
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
// 读取类文件的字节码
while ((bytesNumRead = ins.read(buffer)) != -1)
baos.write(buffer, 0, bytesNumRead);

return baos.toByteArray();
catch (IOException e)
e.printStackTrace();

return null;


private String classNameToPath(String className)
// 得到类文件的完全路径
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";



     如上所示,类 FileSystemClassLoader继承自类java.lang.ClassLoader。在java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。
因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。

  类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。

  加载本地文件系统上的类,示例如下:


package com.example;

public class Sample

private Sample instance;

public void setSample(Object instance)
System.out.println(instance.toString());
this.instance = (Sample) instance;








package classloader;

import java.lang.reflect.Method;

public class ClassIdentity

public static void main(String[] args)
new ClassIdentity().testClassIdentity();


public void testClassIdentity()
String classDataRootPath = "C:\\\\Users\\\\JackZhou\\\\Documents\\\\NetBeansProjects\\\\classloader\\\\build\\\\classes";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.example.Sample";
try
Class class1 = fscl1.loadClass(className); // 加载Sample类
Object obj1 = class1.newInstance(); // 创建对象
Class class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
catch (Exception e)
e.printStackTrace();




运行输出:com.example.Sample@7852e922










2 网络类加载器



  下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。
  类 NetworkClassLoader负责通过网络下载Java类字节代码并定义出Java类。它的实现与FileSystemClassLoader类似。


package classloader;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;

public class NetworkClassLoader extends ClassLoader

private String rootUrl;

public NetworkClassLoader(String rootUrl)
// 指定URL
this.rootUrl = rootUrl;


// 获取类的字节码
@Override
protected Class findClass(String name) throws ClassNotFoundException
byte[] classData = getClassData(name);
if (classData == null)
throw new ClassNotFoundException();
else
return defineClass(name, classData, 0, classData.length);



private byte[] getClassData(String className)
// 从网络上读取的类的字节
String path = classNameToPath(className);
try
URL url = new URL(path);
InputStream ins = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
// 读取类文件的字节
while ((bytesNumRead = ins.read(buffer)) != -1)
baos.write(buffer, 0, bytesNumRead);

return baos.toByteArray();
catch (Exception e)
e.printStackTrace();

return null;


private String classNameToPath(String className)
// 得到类文件的URL
return rootUrl + "/"
+ className.replace('.', '/') + ".class";



       在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用Java反射API。另外一种做法是使用接口。
需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用Java反射API可以直接调用Java类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。我们使用接口的方式。示例如下:



  客户端接口:


package classloader;

public interface Versioned

String getVersion();





package classloader;

public interface ICalculator extends Versioned

String calculate(String expression);


网络上的不同版本的类:





package com.example;

import classloader.ICalculator;

public class CalculatorBasic implements ICalculator

@Override
public String calculate(String expression)
return expression;


@Override
public String getVersion()
return "1.0";





package com.example;

import classloader.ICalculator;

public class CalculatorAdvanced implements ICalculator

@Override
public String calculate(String expression)
return "Result is " + expression;


@Override
public String getVersion()
return "2.0";




在客户端加载网络上的类的过程:





package classloader;

public class CalculatorTest

public static void main(String[] args)
String url = "http://localhost:8080/ClassloaderTest/classes";
NetworkClassLoader ncl = new NetworkClassLoader(url);
String basicClassName = "com.example.CalculatorBasic";
String advancedClassName = "com.example.CalculatorAdvanced";
try
Class clazz = ncl.loadClass(basicClassName); // 加载一个版本的类
ICalculator calculator = (ICalculator) clazz.newInstance(); // 创建对象
System.out.println(calculator.getVersion());
clazz = ncl.loadClass(advancedClassName); // 加载另一个版本的类
calculator = (ICalculator) clazz.newInstance();
System.out.println(calculator.getVersion());
catch (Exception e)
e.printStackTrace();








参考文献:


http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html


http://www.ibm.com/developerworks/cn/java/j-lo-classloader/










推荐阅读
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 如何用JNI技术调用Java接口以及提高Java性能的详解
    本文介绍了如何使用JNI技术调用Java接口,并详细解析了如何通过JNI技术提高Java的性能。同时还讨论了JNI调用Java的private方法、Java开发中使用JNI技术的情况以及使用Java的JNI技术调用C++时的运行效率问题。文章还介绍了JNIEnv类型的使用方法,包括创建Java对象、调用Java对象的方法、获取Java对象的属性等操作。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 从零学Java(10)之方法详解,喷打野你真的没我6!
    本文介绍了从零学Java系列中的第10篇文章,详解了Java中的方法。同时讨论了打野过程中喷打野的影响,以及金色打野刀对经济的增加和线上队友经济的影响。指出喷打野会导致线上经济的消减和影响队伍的团结。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板
    本文介绍了在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板的方法和步骤,包括将ResourceDictionary添加到页面中以及在ResourceDictionary中实现模板的构建。通过本文的阅读,读者可以了解到在Xamarin XAML语言中构建控件模板的具体操作步骤和语法形式。 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了如何将CIM_DateTime解析为.Net DateTime,并分享了解析过程中可能遇到的问题和解决方法。通过使用DateTime.ParseExact方法和适当的格式字符串,可以成功解析CIM_DateTime字符串。同时还提供了关于WMI和字符串格式的相关信息。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
author-avatar
mobiledu2502862217
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有